# Copyright (C) 2018-2022 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

import argparse
import ast
import logging as log
import os
import re
from collections import OrderedDict
from distutils.util import strtobool
from itertools import zip_longest
from typing import List, Union

import numpy as np

from openvino.tools.mo.front.common.partial_infer.utils import mo_array
from openvino.tools.mo.front.extractor import split_node_in_port
from openvino.tools.mo.middle.passes.convert_data_type import destination_type_to_np_data_type
from openvino.tools.mo.utils import import_extensions
from openvino.tools.mo.utils.error import Error
from openvino.tools.mo.utils.utils import refer_to_faq_msg, get_mo_root_dir
from openvino.tools.mo.utils.version import get_version


class DeprecatedStoreTrue(argparse.Action):
    def __init__(self, nargs=0, **kw):
        super().__init__(nargs=nargs, **kw)

    def __call__(self, parser, namespace, values, option_string=None):
        dep_msg = "Use of deprecated cli option {} detected. Option use in the following releases will be fatal. ".format(option_string)
        if 'fusing' in option_string:
            dep_msg += 'Please use --finegrain_fusing cli option instead'
        log.error(dep_msg, extra={'is_warning': True})
        setattr(namespace, self.dest, True)


class DeprecatedOptionCommon(argparse.Action):
    def __call__(self, parser, args, values, option_string):
       dep_msg = "Use of deprecated cli option {} detected. Option use in the following releases will be fatal. ".format(option_string)
       log.error(dep_msg, extra={'is_warning': True})
       setattr(args, self.dest, values)


class IgnoredAction(argparse.Action):
    def __init__(self, nargs=0, **kw):
        super().__init__(nargs=nargs, **kw)

    def __call__(self, parser, namespace, values, option_string=None):
        dep_msg = "Use of removed cli option '{}' detected. The option is ignored. ".format(option_string)
        log.error(dep_msg, extra={'is_warning': True})
        setattr(namespace, self.dest, True)


def canonicalize_and_check_paths(values: Union[str, List[str]], param_name,
                                 try_mo_root=False, check_existance=True) -> List[str]:
    if values is not None:
        list_of_values = list()
        if isinstance(values, str):
            if values != "":
                list_of_values = values.split(',')
        elif isinstance(values, list):
            list_of_values = values
        else:
            raise Error('Unsupported type of command line parameter "{}" value'.format(param_name))

        if not check_existance:
            return [get_absolute_path(path) for path in list_of_values]

        for idx, val in enumerate(list_of_values):
            list_of_values[idx] = val

            error_msg = 'The value for command line parameter "{}" must be existing file/directory, ' \
                        'but "{}" does not exist.'.format(param_name, val)
            if os.path.exists(val):
                continue
            elif not try_mo_root or val == '':
                raise Error(error_msg)
            elif try_mo_root:
                path_from_mo_root = get_mo_root_dir() + '/mo/' + val
                list_of_values[idx] = path_from_mo_root
                if not os.path.exists(path_from_mo_root):
                    raise Error(error_msg)

        return [get_absolute_path(path) for path in list_of_values]


class CanonicalizePathAction(argparse.Action):
    """
    Expand user home directory paths and convert relative-paths to absolute.
    """

    def __call__(self, parser, namespace, values, option_string=None):
        list_of_paths = canonicalize_and_check_paths(values, param_name=option_string,
                                                     try_mo_root=False, check_existance=False)
        setattr(namespace, self.dest, ','.join(list_of_paths))


class CanonicalizeTransformationPathCheckExistenceAction(argparse.Action):
    """
    Convert relative to the current and relative to mo root paths to absolute
    and check specified file or directory existence.
    """

    def __call__(self, parser, namespace, values, option_string=None):
        list_of_paths = canonicalize_and_check_paths(values, param_name=option_string,
                                                     try_mo_root=True, check_existance=True)
        setattr(namespace, self.dest, ','.join(list_of_paths))


class CanonicalizePathCheckExistenceAction(argparse.Action):
    """
    Expand user home directory paths and convert relative-paths to absolute and check specified file or directory
    existence.
    """

    def __call__(self, parser, namespace, values, option_string=None):
        list_of_paths = canonicalize_and_check_paths(values, param_name=option_string,
                                                     try_mo_root=False, check_existance=True)
        setattr(namespace, self.dest, ','.join(list_of_paths))


class CanonicalizePathCheckExistenceIfNeededAction(CanonicalizePathCheckExistenceAction):

    def __call__(self, parser, namespace, values, option_string=None):
        if values is not None:
            if isinstance(values, str):
                if values != "":
                    super().__call__(parser, namespace, values, option_string)
                else:
                    setattr(namespace, self.dest, values)


class DeprecatedCanonicalizePathCheckExistenceAction(CanonicalizePathCheckExistenceAction):
    def __call__(self, parser, namespace, values, option_string=None):
        dep_msg = "Use of deprecated cli option {} detected. Option use in the following releases will be fatal. ".format(
            option_string)
        if 'tensorflow_use_custom_operations_config' in option_string:
            dep_msg += 'Please use --transformations_config cli option instead'
        if 'mean_file' in option_string or 'mean_offset' in option_string:
            dep_msg += 'Please use --mean_values cli option instead.'
        log.error(dep_msg, extra={'is_warning': True})
        super().__call__(parser, namespace, values, option_string)


def readable_file(path: str):
    """
    Check that specified path is a readable file.
    :param path: path to check
    :return: path if the file is readable
    """
    if not os.path.isfile(path):
        raise Error('The "{}" is not existing file'.format(path))
    elif not os.access(path, os.R_OK):
        raise Error('The "{}" is not readable'.format(path))
    else:
        return path


def readable_file_or_dir(path: str):
    """
    Check that specified path is a readable file or directory.
    :param path: path to check
    :return: path if the file/directory is readable
    """
    if not os.path.isfile(path) and not os.path.isdir(path):
        raise Error('The "{}" is not existing file or directory'.format(path))
    elif not os.access(path, os.R_OK):
        raise Error('The "{}" is not readable'.format(path))
    else:
        return path


def readable_dirs(paths: str):
    """
    Checks that comma separated list of paths are readable directories.
    :param paths: comma separated list of paths.
    :return: comma separated list of paths.
    """
    paths_list = [readable_dir(path) for path in paths.split(',')]
    return ','.join(paths_list)


def readable_dirs_or_empty(paths: str):
    """
    Checks that comma separated list of paths are readable directories of if it is empty.
    :param paths: comma separated list of paths.
    :return: comma separated list of paths.
    """
    if paths:
        return readable_dirs(paths)
    return paths


def readable_dirs_or_files_or_empty(paths: str):
    """
    Checks that comma separated list of paths are readable directories, files or a provided path is empty.
    :param paths: comma separated list of paths.
    :return: comma separated list of paths.
    """
    if paths:
        paths_list = [readable_file_or_dir(path) for path in paths.split(',')]
        return ','.join(paths_list)
    return paths


def readable_dir(path: str):
    """
    Check that specified path is a readable directory.
    :param path: path to check
    :return: path if the directory is readable
    """
    if not os.path.isdir(path):
        raise Error('The "{}" is not existing directory'.format(path))
    elif not os.access(path, os.R_OK):
        raise Error('The "{}" is not readable'.format(path))
    else:
        return path


def writable_dir(path: str):
    """
    Checks that specified directory is writable. The directory may not exist but it's parent or grandparent must exist.
    :param path: path to check that it is writable.
    :return: path if it is writable
    """
    if path is None:
        raise Error('The directory parameter is None')
    if os.path.exists(path):
        if os.path.isdir(path):
            if os.access(path, os.W_OK):
                return path
            else:
                raise Error('The directory "{}" is not writable'.format(path))
        else:
            raise Error('The "{}" is not a directory'.format(path))
    else:
        cur_path = path
        while os.path.dirname(cur_path) != cur_path:
            if os.path.exists(cur_path):
                break
            cur_path = os.path.dirname(cur_path)
        if cur_path == '':
            cur_path = os.path.curdir
        if os.access(cur_path, os.W_OK):
            return path
        else:
            raise Error('The directory "{}" is not writable'.format(cur_path))


def get_common_cli_parser(parser: argparse.ArgumentParser = None):
    if not parser:
        parser = argparse.ArgumentParser()
    common_group = parser.add_argument_group('Framework-agnostic parameters')
    # Common parameters
    common_group.add_argument('--input_model', '-w', '-m',
                              help='Tensorflow*: a file with a pre-trained model ' +
                                   ' (binary or text .pb file after freezing).\n' +
                                   ' Caffe*: a model proto file with model weights',
                              action=CanonicalizePathCheckExistenceAction,
                              type=readable_file_or_dir)
    common_group.add_argument('--model_name', '-n',
                              help='Model_name parameter passed to the final create_ir transform. ' +
                                   'This parameter is used to name ' +
                                   'a network in a generated IR and output .xml/.bin files.')
    common_group.add_argument('--output_dir', '-o',
                              help='Directory that stores the generated IR. ' +
                                   'By default, it is the directory from where the Model Optimizer is launched.',
                              default=get_absolute_path('.'),
                              action=CanonicalizePathAction,
                              type=writable_dir)
    common_group.add_argument('--input_shape',
                              help='Input shape(s) that should be fed to an input node(s) of the model. '
                                   'Shape is defined as a comma-separated list of integer numbers enclosed in '
                                   'parentheses or square brackets, for example [1,3,227,227] or (1,227,227,3), where '
                                   'the order of dimensions depends on the framework input layout of the model. '
                                   'For example, [N,C,H,W] is used for ONNX* models and [N,H,W,C] for TensorFlow* '
                                   'models. The shape can contain undefined dimensions (? or -1) and should fit the dimensions defined in the input '
                                   'operation of the graph. Boundaries of undefined dimension can be specified with '
                                   'ellipsis, for example [1,1..10,128,128]. One boundary can be undefined, for '
                                   'example [1,..100] or [1,3,1..,1..]. If there are multiple inputs in the model, '
                                   '--input_shape should contain definition of shape for each input separated by a '
                                   'comma, for example: [1,3,227,227],[2,4] for a model with two inputs with 4D and 2D '
                                   'shapes. Alternatively, specify shapes with the --input option.')
    common_group.add_argument('--scale', '-s',
                              type=float,
                              help='All input values coming from original network inputs will be ' +
                                   'divided by this ' +
                                   'value. When a list of inputs is overridden by the --input ' +
                                   'parameter, this scale ' +
                                   'is not applied for any input that does not match with ' +
                                   'the original input of the model.' +
                                   'If both --mean_values and --scale  are specified, ' +
                                   'the mean is subtracted first and then scale is applied ' +
                                   'regardless of the order of options in command line.')
    common_group.add_argument('--reverse_input_channels',
                              help='Switch the input channels order from RGB to BGR (or vice versa). Applied to '
                                   'original inputs of the model if and only if a number of channels equals 3. '
                                   'When --mean_values/--scale_values are also specified, reversing of channels will '
                                   'be applied to user\'s input data first, so that numbers in --mean_values '
                                   'and --scale_values go in the order of channels used in the original model. '
                                   'In other words, if both options are specified, then the data flow in the model '
                                   'looks as following: Parameter -> ReverseInputChannels -> Mean apply-> Scale apply -> the original body of the model.',
                              action='store_true')
    common_group.add_argument('--log_level',
                              help='Logger level',
                              choices=['CRITICAL', 'ERROR', 'WARN', 'WARNING', 'INFO',
                                       'DEBUG', 'NOTSET'],
                              default='ERROR')
    common_group.add_argument('--input',
                              help='Quoted list of comma-separated input nodes names with shapes, data types, '
                                   'and values for freezing. The order of inputs in converted model is the same as '
                                   'order of specified operation names. The shape and value are specified as space-separated '
                                   'lists. The data type of input node is specified in braces and '
                                   'can have one of the values: f64 (float64), f32 (float32), f16 (float16), '
                                   'i64 (int64), i32 (int32), u8 (uint8), boolean (bool). Data type is optional. '
                                   'If it\'s not specified explicitly then there are two options: '
                                   'if input node is a parameter, data type is taken from the original node dtype, '
                                   'if input node is not a parameter, data type is set to f32. '
                                   'Example, to set `input_1` with shape [1 100], and Parameter node `sequence_len` '
                                   'with scalar input with value `150`, and boolean input `is_training` with '
                                   '`False` value use the following format: '
                                   '"input_1[1 10],sequence_len->150,is_training->False". '
                                   'Another example, use the following format to set input port 0 of the node '
                                   '`node_name1` with the shape [3 4] as an input node and freeze output port 1 '
                                   'of the node `node_name2` with the value [20 15] of the int32 type and shape [2]: '
                                   '"0:node_name1[3 4],node_name2:1[2]{i32}->[20 15]".')
    common_group.add_argument('--output',
                              help='The name of the output operation of the model. ' +
                                   'For TensorFlow*, do not add :0 to this name.'
                                   'The order of outputs in converted model is the same as order of '
                                   'specified operation names.')
    common_group.add_argument('--mean_values', '-ms',
                              help='Mean values to be used for the input image per channel. ' +
                                   'Values to be provided in the (R,G,B) or [R,G,B] format. ' +
                                   'Can be defined for desired input of the model, for example: ' +
                                   '"--mean_values data[255,255,255],info[255,255,255]". ' +
                                   'The exact meaning and order ' +
                                   'of channels depend on how the original model was trained.',
                              default=())
    common_group.add_argument('--scale_values',
                              help='Scale values to be used for the input image per channel. ' +
                                   'Values are provided in the (R,G,B) or [R,G,B] format. ' +
                                   'Can be defined for desired input of the model, for example: ' +
                                   '"--scale_values data[255,255,255],info[255,255,255]". ' +
                                   'The exact meaning and order ' +
                                   'of channels depend on how the original model was trained.' +
                                   'If both --mean_values and --scale_values are specified, ' +
                                   'the mean is subtracted first and then scale is applied ' +
                                   'regardless of the order of options in command line.',
                              default=())
    common_group.add_argument('--source_layout',
                              help='Layout of the input or output of the model in the framework. Layout can'
                                   ' be specified in the short form, e.g. nhwc, or in complex form, e.g. [n,h,w,c].'
                                   ' Example for many names: '
                                   'in_name1([n,h,w,c]),in_name2(nc),out_name1(n),out_name2(nc). Layout can be '
                                   'partially defined, "?" can be used to specify undefined layout for one dimension, '
                                   '"..." can be used to specify undefined layout for multiple dimensions, for example '
                                   '?c??, nc..., n...c, etc.',
                              default=())
    common_group.add_argument('--target_layout',
                              help='Same as --source_layout, but specifies target layout that will be in the model '
                                   'after processing by ModelOptimizer.',
                              default=())
    common_group.add_argument('--layout',
                              help='Combination of --source_layout and --target_layout. Can\'t be used with either of '
                                   'them. If model has one input it is sufficient to specify layout of this input, for'
                                   ' example --layout nhwc. To specify layouts of many tensors, names must be provided,'
                                   ' for example: --layout name1(nchw),name2(nc). It is possible to instruct '
                                   'ModelOptimizer to change layout, for example: '
                                   '--layout name1(nhwc->nchw),name2(cn->nc). Also "*" in long layout form can be used'
                                   ' to fuse dimensions, for example [n,c,...]->[n*c,...].',
                              default=())
    # TODO: isn't it a weights precision type
    common_group.add_argument('--data_type',
                              help='Data type for all intermediate tensors and weights. ' +
                                   'If original model is in FP32 and --data_type=FP16 is specified, all model weights ' +
                                   'and biases are compressed to FP16.',
                              choices=["FP16", "FP32", "half", "float"],
                              default='float')
    common_group.add_argument('--transform',
                              help='Apply additional transformations. ' +
                                   'Usage: "--transform transformation_name1[args],transformation_name2..." ' +
                                   'where [args] is key=value pairs separated by semicolon. ' +
                                   'Examples: "--transform LowLatency2" or ' +
                                   '          "--transform LowLatency2[use_const_initializer=False]" or ' +
                                   '          "--transform \"MakeStateful[param_res_names='
                                   '{\'input_name_1\':\'output_name_1\',\'input_name_2\':\'output_name_2\'}]\"" ' +
                                   'Available transformations: "LowLatency2", "MakeStateful"',
                              default="")
    common_group.add_argument('--disable_fusing',
                              help='[DEPRECATED] Turn off fusing of linear operations to Convolution.',
                              action=DeprecatedStoreTrue)
    common_group.add_argument('--disable_resnet_optimization',
                              help='[DEPRECATED] Turn off ResNet optimization.',
                              action=DeprecatedStoreTrue, default=False)
    common_group.add_argument('--finegrain_fusing',
                              help='[DEPRECATED] Regex for layers/operations that won\'t be fused. ' +
                                   'Example: --finegrain_fusing Convolution1,.*Scale.*',
                              action=DeprecatedOptionCommon)
    common_group.add_argument('--enable_concat_optimization',
                              help='[DEPRECATED] Turn on Concat optimization.',
                              action=DeprecatedStoreTrue, default=False)
    # we use CanonicalizeDirCheckExistenceAction instead of readable_dirs to handle empty strings
    common_group.add_argument("--extensions",
                              help="Paths or a comma-separated list of paths to libraries (.so or .dll) "
                                   "with extensions. For the legacy MO path (if `--use_legacy_frontend` is used), "
                                   "a directory or a comma-separated list of directories with extensions are supported. "
                                   "To disable all extensions including those that are placed at the default location, "
                                   "pass an empty string.",
                              default=import_extensions.default_path(),
                              action=CanonicalizePathCheckExistenceAction,
                              type=readable_dirs_or_files_or_empty)
    common_group.add_argument("--batch", "-b",
                              type=check_positive,
                              default=None,
                              help="Input batch size")
    common_group.add_argument("--version",
                              action='version',
                              version='Version of Model Optimizer is: {}'.format(get_version()),
                              help="Version of Model Optimizer")

    common_group.add_argument('--silent',
                              help='Prevent any output messages except those that correspond to log level equals '
                                   'ERROR, that can be set with the following option: --log_level. '
                                   'By default, log level is already ERROR. ',
                              action='store_true',
                              default=False)
    common_group.add_argument('--freeze_placeholder_with_value',
                              help='Replaces input layer with constant node with '
                                   'provided value, for example: "node_name->True". '
                                   'It will be DEPRECATED in future releases. '
                                   'Use --input option to specify a value for freezing.',
                              default=None)
    common_group.add_argument('--static_shape',
                              help='Enables IR generation for fixed input shape (folding `ShapeOf` operations and '
                                   'shape-calculating sub-graphs to `Constant`). Changing model input shape using '
                                   'the OpenVINO Runtime API in runtime may fail for such an IR.',
                              action='store_true', default=False)
    common_group.add_argument('--disable_weights_compression',
                              help='[DEPRECATED] Disable compression and store weights with original precision.',
                              action=DeprecatedStoreTrue, default=False)
    common_group.add_argument('--progress',
                              help='Enable model conversion progress display.',
                              action='store_true', default=False)
    common_group.add_argument('--stream_output',
                              help='Switch model conversion progress display to a multiline mode.',
                              action='store_true', default=False)
    common_group.add_argument('--transformations_config',
                              help='Use the configuration file with transformations '
                                   'description. File can be specified as relative path '
                                   'from the current directory, as absolute path or as a'
                                   'relative path from the mo root directory',
                              action=CanonicalizeTransformationPathCheckExistenceAction)
    common_group.add_argument("--use_new_frontend",
                              help='Force the usage of new Frontend of Model Optimizer for model conversion into IR. '
                                   'The new Frontend is C++ based and is available for ONNX* and PaddlePaddle* models. '
                                   'Model optimizer uses new Frontend for ONNX* and PaddlePaddle* by default that means '
                                   '`--use_new_frontend` and `--use_legacy_frontend` options are not specified.',
                              action='store_true', default=False)
    common_group.add_argument("--use_legacy_frontend",
                              help='Force the usage of legacy Frontend of Model Optimizer for model conversion into IR. '
                                   'The legacy Frontend is Python based and is available for TensorFlow*, ONNX*, MXNet*, '
                                   'Caffe*, and Kaldi* models.',
                              action='store_true', default=False)
    return parser


def get_common_cli_options(model_name):
    d = OrderedDict()
    d['input_model'] = '- Path to the Input Model'
    d['output_dir'] = ['- Path for generated IR', lambda x: x if x != '.' else os.getcwd()]
    d['model_name'] = ['- IR output name', lambda x: x if x else model_name]
    d['log_level'] = '- Log level'
    d['batch'] = ['- Batch', lambda x: x if x else 'Not specified, inherited from the model']
    d['input'] = ['- Input layers', lambda x: x if x else 'Not specified, inherited from the model']
    d['output'] = ['- Output layers', lambda x: x if x else 'Not specified, inherited from the model']
    d['input_shape'] = ['- Input shapes', lambda x: x if x else 'Not specified, inherited from the model']
    d['source_layout'] = ['- Source layout', lambda x: x if x else 'Not specified']
    d['target_layout'] = ['- Target layout', lambda x: x if x else 'Not specified']
    d['layout'] = ['- Layout', lambda x: x if x else 'Not specified']
    d['mean_values'] = ['- Mean values', lambda x: x if x else 'Not specified']
    d['scale_values'] = ['- Scale values', lambda x: x if x else 'Not specified']
    d['scale'] = ['- Scale factor', lambda x: x if x else 'Not specified']
    d['data_type'] = ['- Precision of IR', lambda x: 'FP32' if x == 'float' else 'FP16' if x == 'half' else x]
    d['disable_fusing'] = ['- Enable fusing', lambda x: not x]
    d['transform'] = ['- User transformations', lambda x: x if x else 'Not specified']
    d['reverse_input_channels'] = '- Reverse input channels'
    d['static_shape'] = '- Enable IR generation for fixed input shape'
    d['transformations_config'] = '- Use the transformations config file'
    return d


def get_advanced_cli_options():
    d = OrderedDict()
    d['use_legacy_frontend'] = '- Force the usage of legacy Frontend of Model Optimizer for model conversion into IR'
    d['use_new_frontend'] = '- Force the usage of new Frontend of Model Optimizer for model conversion into IR'
    return d


def get_caffe_cli_options():
    d = {
        'input_proto': ['- Path to the Input prototxt', lambda x: x],
        'caffe_parser_path': ['- Path to Python Caffe* parser generated from caffe.proto', lambda x: x],
        'mean_file': ['- Path to a mean file', lambda x: x if x else 'Not specified'],
        'mean_file_offsets': ['- Offsets for a mean file', lambda x: x if x else 'Not specified'],
        'k': '- Path to CustomLayersMapping.xml',
        'disable_resnet_optimization': ['- Enable resnet optimization', lambda x: not x],
    }

    return OrderedDict(sorted(d.items(), key=lambda t: t[0]))


def get_tf_cli_options():
    d = {
        'input_model_is_text': '- Input model in text protobuf format',
        'tensorflow_custom_operations_config_update': '- Update the configuration file with input/output node names',
        'tensorflow_use_custom_operations_config': '- Use the config file',
        'tensorflow_object_detection_api_pipeline_config': '- Use configuration file used to generate the model with '
                                                           'Object Detection API',
        'tensorflow_custom_layer_libraries': '- List of shared libraries with TensorFlow custom layers implementation',
        'tensorboard_logdir': '- Path to model dump for TensorBoard'
    }

    return OrderedDict(sorted(d.items(), key=lambda t: t[0]))


def get_mxnet_cli_options():
    d = {
        'input_symbol': '- Deploy-ready symbol file',
        'nd_prefix_name': '- Prefix name for args.nd and argx.nd files',
        'pretrained_model_name': '- Pretrained model to be merged with the .nd files',
        'save_params_from_nd': '- Enable saving built parameters file from .nd files',
        'legacy_mxnet_model': '- Enable MXNet loader for models trained with MXNet version lower than 1.0.0',
    }

    return OrderedDict(sorted(d.items(), key=lambda t: t[0]))


def get_kaldi_cli_options():
    d = {
        'counts': '- A file name with full path to the counts file or empty string if you want to use counts from model',
        'remove_output_softmax': '- Removes the SoftMax layer that is the output layer',
        'remove_memory': '- Removes the Memory layer and use additional inputs and outputs instead'
    }

    return OrderedDict(sorted(d.items(), key=lambda t: t[0]))


def get_onnx_cli_options():
    d = {
    }

    return OrderedDict(sorted(d.items(), key=lambda t: t[0]))


def get_params_with_paths_list():
    return ['input_model', 'output_dir', 'caffe_parser_path', 'extensions', 'k', 'output_dir',
            'input_checkpoint', 'input_meta_graph', 'input_proto', 'input_symbol', 'mean_file',
            'mean_file_offsets', 'pretrained_model_name', 'saved_model_dir', 'tensorboard_logdir',
            'tensorflow_custom_layer_libraries', 'tensorflow_custom_operations_config_update',
            'tensorflow_object_detection_api_pipeline_config', 'tensorflow_use_custom_operations_config',
            'transformations_config']


def get_caffe_cli_parser(parser: argparse.ArgumentParser = None):
    """
    Specifies cli arguments for Model Optimizer for Caffe*

    Returns
    -------
        ArgumentParser instance
    """
    if not parser:
        parser = argparse.ArgumentParser(usage='%(prog)s [options]')
        get_common_cli_parser(parser=parser)

    caffe_group = parser.add_argument_group('Caffe*-specific parameters')

    caffe_group.add_argument('--input_proto', '-d',
                             help='Deploy-ready prototxt file that contains a topology structure ' +
                                  'and layer attributes',
                             type=str,
                             action=CanonicalizePathCheckExistenceAction)
    caffe_group.add_argument('--caffe_parser_path',
                             help='Path to Python Caffe* parser generated from caffe.proto',
                             type=str,
                             default=os.path.join(os.path.dirname(__file__), os.pardir, 'front', 'caffe', 'proto'),
                             action=CanonicalizePathCheckExistenceAction)
    caffe_group.add_argument('-k',
                             help='Path to CustomLayersMapping.xml to register custom layers',
                             type=str,
                             default=os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, 'extensions', 'front', 'caffe',
                                                  'CustomLayersMapping.xml'),
                             action=CanonicalizePathCheckExistenceAction)
    caffe_group.add_argument('--mean_file', '-mf',
                             help='[DEPRECATED] ' +
                                  'Mean image to be used for the input. Should be a binaryproto file',
                             default=None,
                             action=DeprecatedCanonicalizePathCheckExistenceAction)
    caffe_group.add_argument('--mean_file_offsets', '-mo',
                             help='[DEPRECATED] ' +
                                  'Mean image offsets to be used for the input binaryproto file. ' +
                                  'When the mean image is bigger than the expected input, it is cropped. By default, centers ' +
                                  'of the input image and the mean image are the same and the mean image is cropped by ' +
                                  'dimensions of the input image. The format to pass this option is the following: "-mo (x,y)". In this ' +
                                  'case, the mean file is cropped by dimensions of the input image with offset (x,y) ' +
                                  'from the upper left corner of the mean image',
                             default=None)
    caffe_group.add_argument('--disable_omitting_optional',
                             help='Disable omitting optional attributes to be used for custom layers. ' +
                                  'Use this option if you want to transfer all attributes of a custom layer to IR. ' +
                                  'Default behavior is to transfer the attributes with default values and the attributes defined by the user to IR.',
                             action='store_true',
                             default=False)
    caffe_group.add_argument('--enable_flattening_nested_params',
                             help='Enable flattening optional params to be used for custom layers. ' +
                                  'Use this option if you want to transfer attributes of a custom layer to IR with flattened nested parameters. ' +
                                  'Default behavior is to transfer the attributes without flattening nested parameters.',
                             action='store_true',
                             default=False)
    return parser


def get_tf_cli_parser(parser: argparse.ArgumentParser = None):
    """
    Specifies cli arguments for Model Optimizer for TF

    Returns
    -------
        ArgumentParser instance
    """
    if not parser:
        parser = argparse.ArgumentParser(usage='%(prog)s [options]')
        get_common_cli_parser(parser=parser)

    tf_group = parser.add_argument_group('TensorFlow*-specific parameters')
    tf_group.add_argument('--input_model_is_text',
                          help='TensorFlow*: treat the input model file as a text protobuf format. If not specified, ' +
                               'the Model Optimizer treats it as a binary file by default.',
                          action='store_true')
    tf_group.add_argument('--input_checkpoint', type=str, default=None, help="TensorFlow*: variables file to load.",
                          action=CanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--input_meta_graph',
                          help='Tensorflow*: a file with a meta-graph of the model before freezing',
                          action=CanonicalizePathCheckExistenceAction,
                          type=readable_file)
    tf_group.add_argument('--saved_model_dir', default=None,
                          help='TensorFlow*: directory with a model in SavedModel format '
                               'of TensorFlow 1.x or 2.x version.',
                          action=CanonicalizePathCheckExistenceAction,
                          type=readable_dirs)
    tf_group.add_argument('--saved_model_tags', type=str, default=None,
                          help="Group of tag(s) of the MetaGraphDef to load, in string format, separated by ','. "
                               "For tag-set contains multiple tags, all tags must be passed in.")
    tf_group.add_argument('--tensorflow_custom_operations_config_update',
                          help='TensorFlow*: update the configuration file with node name patterns with input/output '
                               'nodes information.',
                          action=CanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--tensorflow_use_custom_operations_config',
                              help='Use the configuration file with custom operation description.',
                              action=DeprecatedCanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--tensorflow_object_detection_api_pipeline_config',
                          help='TensorFlow*: path to the pipeline configuration file used to generate model created '
                               'with help of Object Detection API.',
                          action=CanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--tensorboard_logdir',
                          help='TensorFlow*: dump the input graph to a given directory that should be used with TensorBoard.',
                          default=None,
                          action=CanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--tensorflow_custom_layer_libraries',
                          help='TensorFlow*: comma separated list of shared libraries with TensorFlow* custom '
                               'operations implementation.',
                          default=None,
                          action=CanonicalizePathCheckExistenceAction)
    tf_group.add_argument('--disable_nhwc_to_nchw',
                          help='[DEPRECATED] Disables the default translation from NHWC to NCHW. Since 2022.1 this option '
                               'is deprecated and used only to maintain backward compatibility with previous releases.',
                          action=DeprecatedStoreTrue, default=False)
    return parser


def get_mxnet_cli_parser(parser: argparse.ArgumentParser = None):
    """
    Specifies cli arguments for Model Optimizer for MXNet*

    Returns
    -------
        ArgumentParser instance
    """
    if not parser:
        parser = argparse.ArgumentParser(usage='%(prog)s [options]')
        get_common_cli_parser(parser=parser)

    mx_group = parser.add_argument_group('Mxnet-specific parameters')

    mx_group.add_argument('--input_symbol',
                          help='Symbol file (for example, model-symbol.json) that contains a topology structure ' +
                               'and layer attributes',
                          type=str,
                          action=CanonicalizePathCheckExistenceAction)
    mx_group.add_argument("--nd_prefix_name",
                          help="Prefix name for args.nd and argx.nd files.",
                          default=None)
    mx_group.add_argument("--pretrained_model_name",
                          help="Name of a pretrained MXNet model without extension and epoch number. This model will be merged with args.nd and argx.nd files",
                          default=None)
    mx_group.add_argument("--save_params_from_nd",
                          action='store_true',
                          help="Enable saving built parameters file from .nd files")
    mx_group.add_argument("--legacy_mxnet_model",
                          action='store_true',
                          help="Enable MXNet loader to make a model compatible with the latest MXNet version. Use only if your model was trained with MXNet version lower than 1.0.0")
    mx_group.add_argument("--enable_ssd_gluoncv",
                          action='store_true',
                          help="Enable pattern matchers replacers for converting gluoncv ssd topologies.",
                          default=False)

    return parser


def get_kaldi_cli_parser(parser: argparse.ArgumentParser = None):
    """
    Specifies cli arguments for Model Optimizer for MXNet*

    Returns
    -------
        ArgumentParser instance
    """
    if not parser:
        parser = argparse.ArgumentParser(usage='%(prog)s [options]')
        get_common_cli_parser(parser=parser)

    kaldi_group = parser.add_argument_group('Kaldi-specific parameters')

    kaldi_group.add_argument("--counts",
                             help="Path to the counts file",
                             default=None,
                             action=CanonicalizePathCheckExistenceIfNeededAction)

    kaldi_group.add_argument("--remove_output_softmax",
                             help="Removes the SoftMax layer that is the output layer",
                             action='store_true',
                             default=False)

    kaldi_group.add_argument("--remove_memory",
                             help="Removes the Memory layer and use additional inputs outputs instead",
                             action='store_true',
                             default=False)
    return parser


def get_onnx_cli_parser(parser: argparse.ArgumentParser = None):
    """
    Specifies cli arguments for Model Optimizer for ONNX

    Returns
    -------
        ArgumentParser instance
    """
    if not parser:
        parser = argparse.ArgumentParser(usage='%(prog)s [options]')
        get_common_cli_parser(parser=parser)

    return parser


def get_all_cli_parser(frontEndManager=None):
    """
    Specifies cli arguments for Model Optimizer

    Returns
    -------
        ArgumentParser instance
    """
    parser = argparse.ArgumentParser(usage='%(prog)s [options]')

    frameworks = list(set(['tf', 'caffe', 'mxnet', 'kaldi', 'onnx'] +
                          (get_available_front_ends(frontEndManager) if frontEndManager else [])))

    parser.add_argument('--framework',
                        help='Name of the framework used to train the input model.',
                        type=str,
                        choices=frameworks)

    get_common_cli_parser(parser=parser)

    get_tf_cli_parser(parser=parser)
    get_caffe_cli_parser(parser=parser)
    get_mxnet_cli_parser(parser=parser)
    get_kaldi_cli_parser(parser=parser)
    get_onnx_cli_parser(parser=parser)

    return parser


def remove_data_type_from_input_value(input_value: str):
    """
    Removes the type specification from the input string. The type specification is a string enclosed with curly braces.
    :param input_value: string passed as input to the --input command line parameter
    :return: string without type specification
    """
    return re.sub(r'\{.*\}', '', input_value)


def get_data_type_from_input_value(input_value: str):
    """
    Returns the numpy data type corresponding to the data type specified in the input value string
    :param input_value: string passed as input to the --input command line parameter
    :return: the corresponding numpy data type and None if the data type is not specified in the input value
    """
    data_type_match = re.match(r'.*\{(.*)\}.*', input_value)
    return destination_type_to_np_data_type(data_type_match.group(1)) if data_type_match is not None else None


def remove_shape_from_input_value(input_value: str):
    """
    Removes the shape specification from the input string. The shape specification is a string enclosed with square
    brackets.
    :param input_value: string passed as input to the --input command line parameter
    :return: string without shape specification
    """
    assert '->' not in input_value, 'The function should not be called for input_value with constant value specified'
    return re.sub(r'[(\[]([0-9\.?  -]*)[)\]]', '', input_value)


def get_shape_from_input_value(input_value: str):
    """
    Returns the list of tuples corresponding to the shape specified in the input value string
    :param input_value: string passed as input to the --input command line parameter
    :return: the corresponding shape and None if the shape is not specified in the input value
    """
    # remove the tensor value from the input_value first
    input_value = input_value.split('->')[0]

    # parse shape
    shape = re.findall(r'[(\[]([0-9\.\?  -]*)[)\]]', input_value)
    if len(shape) == 0:
        shape = None
    elif len(shape) == 1 and shape[0] in ['', ' ']:
        shape = ()
    elif len(shape) == 1:
        shape = tuple(map(parse_dimension, shape[0].split(' ')))
    else:
        raise Error("Wrong syntax to specify shape. Use --input "
                    "\"node_name[shape]->value\"")
    return shape


def get_node_name_with_port_from_input_value(input_value: str):
    """
    Returns the node name (optionally with input/output port) from the input value
    :param input_value: string passed as input to the --input command line parameter
    :return: the corresponding node name with input/output port
    """
    return remove_shape_from_input_value(remove_data_type_from_input_value(input_value.split('->')[0]))


def get_value_from_input_value(input_value: str):
    """
    Returns the value from the input value string
    :param input_value: string passed as input to the --input command line parameter
    :return: the corresponding value or None if it is not specified
    """
    parts = input_value.split('->')
    value = None
    if len(parts) == 2:
        value = parts[1]
        if value[0] == '[' and value[-1] != ']' or value[0] != '[' and value[-1] == ']':
            raise Error("Wrong syntax to specify value. Use --input \"node_name[shape]->value\"")
        if '[' in value.strip(' '):
            value = value.replace('[', '').replace(']', '').split(' ')
        if not isinstance(value, list):
            value = ast.literal_eval(value)
    elif len(parts) > 2:
        raise Error("Wrong syntax to specify value. Use --input \"node_name[shape]->value\"")
    return value


def parse_input_value(input_value: str):
    """
    Parses a value of the --input command line parameter and gets a node name, shape and value.
    The node name includes a port if it is specified.
    Shape and value is equal to None if they are not specified.
    Parameters
    ----------
    input_value
        string with a specified node name, shape, value and data_type.
        E.g. 'node_name:0[4]{fp32}->[1.0 2.0 3.0 4.0]'

    Returns
    -------
        Node name, shape, value, data type
        E.g. 'node_name:0', '4', [1.0 2.0 3.0 4.0], np.float32
    """
    data_type = get_data_type_from_input_value(input_value)
    node_name = get_node_name_with_port_from_input_value(input_value)
    value = get_value_from_input_value(input_value)
    shape = get_shape_from_input_value(input_value)
    value_size = np.prod(len(value)) if isinstance(value, list) else 1

    if value is not None and shape is not None:
        for dim in shape:
            if isinstance(dim, tuple) or dim == -1:
                raise Error("Cannot freeze input with dynamic shape: {}".format(shape))

    if shape is not None and value is not None and np.prod(shape) != value_size:
        raise Error("The shape '{}' of the input node '{}' does not correspond to the number of elements '{}' in the "
                    "value: {}".format(shape, node_name, value_size, value))
    return node_name, shape, value, data_type


def split_str_avoiding_square_brackets(s: str) -> list:
    """
    Splits a string by comma, but skips commas inside square brackets.
    :param s: string to split
    :return: list of strings split by comma
    """
    res = list()
    skipping = 0
    last_idx = 0
    for i, c in enumerate(s):
        if c == '[':
            skipping += 1
        elif c == ']':
            skipping -= 1
        elif c == ',' and skipping == 0:
            res.append(s[last_idx:i])
            last_idx = i + 1
    res.append(s[last_idx:])
    return res


def split_layouts_by_arrow(s: str) -> tuple:
    """
    Splits a layout string by first arrow (->).
    :param s: string to split
    :return: tuple containing source and target layouts
    """
    arrow = s.find('->')
    if arrow != -1:
        source_layout = s[:arrow]
        target_layout = s[arrow + 2:]
        if source_layout == '':
            source_layout = None
        if target_layout == '':
            target_layout = None
        return source_layout, target_layout
    else:
        return s, None


def validate_layout(layout: str):
    """
    Checks if layout is of valid format.
    :param layout: string containing layout
    :raises: if layout is incorrect
    """
    valid_layout_re = re.compile(r'\[?[^\[\]\(\)\s]*\]?')
    if layout and not valid_layout_re.fullmatch(layout):
        raise Error('Invalid layout parsed: {}'.format(layout))


def write_found_layout(name: str, found_layout: str, parsed: dict, dest: str = None):
    """
    Writes found layout data to the 'parsed' dict.
    :param name: name of the node to add layout
    :param found_layout: string containing layout for the node
    :param parsed: dict where result will be stored
    :param dest: type of the command line:
      * 'source' is --source_layout
      * 'target' is --target_layout
      * None is --layout
    """
    s_layout = None
    t_layout = None
    if name in parsed:
        s_layout = parsed[name]['source_layout']
        t_layout = parsed[name]['target_layout']
    if dest == 'source':
        s_layout = found_layout
    elif dest == 'target':
        t_layout = found_layout
    else:
        s_layout, t_layout = split_layouts_by_arrow(found_layout)
    validate_layout(s_layout)
    validate_layout(t_layout)
    parsed[name] = {'source_layout': s_layout, 'target_layout': t_layout}


def parse_layouts_by_destination(s: str, parsed: dict, dest: str = None) -> None:
    """
    Parses layout command line to get all names and layouts from it. Adds all found data in the 'parsed' dict.
    :param s: string to parse
    :param parsed: dict where result will be stored
    :param dest: type of the command line:
      * 'source' is --source_layout
      * 'target' is --target_layout
      * None is --layout
    """
    list_s = split_str_avoiding_square_brackets(s)
    if len(list_s) == 1 and (list_s[0][-1] not in ')]' or (list_s[0][0] == '[' and list_s[0][-1] == ']')):
        # single layout case
        write_found_layout('', list_s[0], parsed, dest)
    else:
        for layout_str in list_s:
            # case for: "name1(nhwc->[n,c,h,w])"
            p1 = re.compile(r'(\S+)\((\S+)\)')
            m1 = p1.match(layout_str)
            # case for: "name1[n,h,w,c]->[n,c,h,w]"
            p2 = re.compile(r'(\S+)(\[\S*\])')
            m2 = p2.match(layout_str)
            if m1:
                found_g = m1.groups()
            elif m2:
                found_g = m2.groups()
            else:
                error_msg = "Invalid usage of --{}layout parameter. Please use following syntax for each tensor " \
                            "or operation name:" \
                            "\n  name(nchw)" \
                            "\n  name[n,c,h,w]".format(dest + '_' if dest else '')
                if dest is None:
                    error_msg += "\n  name(nhwc->[n,h,w,c])" \
                                 "\n  name[n,h,w,c]->[n,c,h,w]"
                raise Error(error_msg)
            write_found_layout(found_g[0], found_g[1], parsed, dest)


def get_layout_values(argv_layout: str = '', argv_source_layout: str = '', argv_target_layout: str = ''):
    """
    Parses layout string.
    :param argv_layout: string with a list of layouts passed as a --layout.
    :param argv_source_layout: string with a list of layouts passed as a --source_layout.
    :param argv_target_layout: string with a list of layouts passed as a --target_layout.
    :return: dict with names and layouts associated
    """
    if argv_layout and (argv_source_layout or argv_target_layout):
        raise Error("--layout is used as well as --source_layout and/or --target_layout which is not allowed, please "
                    "use one of them.")
    res = {}
    if argv_layout:
        parse_layouts_by_destination(argv_layout, res)
    if argv_source_layout:
        parse_layouts_by_destination(argv_source_layout, res, 'source')
    if argv_target_layout:
        parse_layouts_by_destination(argv_target_layout, res, 'target')
    return res


def get_freeze_placeholder_values(argv_input: str, argv_freeze_placeholder_with_value: str):
    """
    Parses values for placeholder freezing and input node names

    Parameters
    ----------
    argv_input
        string with a list of input layers: either an empty string, or strings separated with comma.
        'node_name1[shape1]->value1,node_name2[shape2]->value2,...'
    argv_freeze_placeholder_with_value
        string with a list of input shapes: either an empty string, or tuples separated with comma.
        'placeholder_name1->value1, placeholder_name2->value2,...'

    Returns
    -------
        parsed placeholders with values for freezing
        input nodes cleaned from shape info
    """
    placeholder_values = {}
    input_node_names = None

    if argv_freeze_placeholder_with_value is not None:
        for plh_with_value in argv_freeze_placeholder_with_value.split(','):
            plh_with_value = plh_with_value.split('->')
            if len(plh_with_value) != 2:
                raise Error("Wrong replacement syntax. Use --freeze_placeholder_with_value "
                            "\"node1_name->value1,node2_name->value2\"")
            node_name = plh_with_value[0]
            value = plh_with_value[1]
            if node_name in placeholder_values and placeholder_values[node_name] != value:
                raise Error("Overriding replacement value of the placeholder with name '{}': old value = {}, new value = {}"
                            ".".format(node_name, placeholder_values[node_name], value))
            if '[' in value.strip(' '):
                value = value.replace('[', '').replace(']', '').split(' ')
            placeholder_values[node_name] = value

    if argv_input is not None:
        input_node_names = ''
        # walkthrough all input values and save values for freezing
        for input_value in argv_input.split(','):
            node_name, _, value, _ = parse_input_value(input_value)
            input_node_names = input_node_names + ',' + node_name  if input_node_names != '' else node_name
            if value is None: # no value is specified for freezing
                continue
            if node_name in placeholder_values and placeholder_values[node_name] != value:
                raise Error("Overriding replacement value of the placeholder with name '{}': old value = {}, new value = {}"
                            ".".format(node_name, placeholder_values[node_name], value))
            placeholder_values[node_name] = value

    return placeholder_values, input_node_names


def parse_dimension(dim: str):
    if '..' in dim:
        numbers_reg = r'^[0-9]+$'
        dims = dim.split('..')
        match_res0 = re.match(numbers_reg, dims[0])
        match_res1 = re.match(numbers_reg, dims[1])
        if len(dims[0].strip()) > 0 and match_res0 is None:
            Error("Incorrect min value of dimension '{}'".format(dims[0]))
        if len(dims[1].strip()) > 0 and match_res1 is None:
            Error("Incorrect max value of dimension '{}'".format(dims[1]))

        min_val = np.int64(dims[0]) if match_res0 else np.int64(0)
        max_val = np.int64(dims[1]) if match_res1 else np.iinfo(np.int64).max
        assert min_val >= 0, "Incorrect min value of the dimension {}".format(dim)

        if min_val == np.int64(0) and max_val == np.iinfo(np.int64).max:
            return np.int64(-1)

        assert min_val < max_val, "Min value should be less than max value. Got min value: {}, " \
                                  "max value: {}".format(min_val, max_val)

        return min_val, max_val
    if '?' in dim:
        return np.int64(-1)
    return np.int64(dim)


def get_placeholder_shapes(argv_input: str, argv_input_shape: str, argv_batch=None):
    """
    Parses input layers names and input shapes from the cli and returns the parsed object.
    All shapes are specified only through one command line option either --input or --input_shape.

    Parameters
    ----------
    argv_input
        string with a list of input layers: either an empty string, or strings separated with comma.
        E.g. 'inp1,inp2', 'node_name1[shape1]->value1,node_name2[shape2]->value2'
    argv_input_shape
        string with a list of input shapes: either an empty string, or tuples separated with comma.
        E.g. '[1,2],[3,4]'.
        Only positive integers are accepted.
        '?' marks dynamic dimension.
        Partial shape is specified with ellipsis. E.g. '[1..10,2,3]'
    argv_batch
        integer that overrides batch size in input shape

    Returns
    -------
        parsed shapes in form of {'name of input':tuple} if names of inputs are provided with shapes
        parsed shapes in form of {'name of input':None} if names of inputs are provided without shapes
        tuple if only one shape is provided and no input name
        None if neither shape nor input were provided
    """
    if argv_input_shape and argv_batch:
        raise Error("Both --input_shape and --batch were provided. Please provide only one of them. " +
                    refer_to_faq_msg(56))

    # attempt to extract shapes from --input parameters
    placeholder_shapes = dict()
    placeholder_data_types = dict()
    are_shapes_specified_through_input = False
    inputs_list = list()
    if argv_input:
        for input_value in argv_input.split(','):
            node_name, shape, _, data_type = parse_input_value(input_value)
            placeholder_shapes[node_name] = shape
            inputs_list.append(node_name)
            if data_type is not None:
                placeholder_data_types[node_name] = data_type
            if shape is not None:
                are_shapes_specified_through_input = True

    if argv_input_shape and are_shapes_specified_through_input:
        raise Error("Shapes are specified using both --input and --input_shape command-line parameters, but only one "
                    "parameter is allowed.")

    if argv_batch and are_shapes_specified_through_input:
        raise Error("Shapes are specified using both --input and --batch command-line parameters, but only one "
                    "parameter is allowed.")

    if are_shapes_specified_through_input:
        return inputs_list, placeholder_shapes, placeholder_data_types

    shapes = list()
    inputs = list()
    inputs_list = list()
    placeholder_shapes = None

    range_reg = r'([0-9]*\.\.[0-9]*)'
    first_digit_reg = r'([0-9 ]+|-1|\?|{})'.format(range_reg)
    next_digits_reg = r'(,{})*'.format(first_digit_reg)
    tuple_reg = r'((\({}{}\))|(\[{}{}\]))'.format(first_digit_reg, next_digits_reg,
                                                  first_digit_reg, next_digits_reg)
    if argv_input_shape:
        full_reg = r'^{}(\s*,\s*{})*$|^$'.format(tuple_reg, tuple_reg)
        if not re.match(full_reg, argv_input_shape):
            raise Error('Input shape "{}" cannot be parsed. ' + refer_to_faq_msg(57), argv_input_shape)
        shapes = re.findall(r'[(\[]([0-9,\.\? -]+)[)\]]', argv_input_shape)

    if argv_input:
        inputs = argv_input.split(',')
    inputs = [remove_data_type_from_input_value(inp) for inp in inputs]

    # check number of shapes with no input provided
    if argv_input_shape and not argv_input:
        if len(shapes) > 1:
            raise Error('Please provide input layer names for input layer shapes. ' + refer_to_faq_msg(58))
        else:
            placeholder_shapes = tuple(map(parse_dimension, shapes[0].split(',')))
    # check if number of shapes does not match number of passed inputs
    elif argv_input and (len(shapes) == len(inputs) or len(shapes) == 0):
        # clean inputs from values for freezing
        inputs_without_value = list(map(lambda x: x.split('->')[0], inputs))
        placeholder_shapes = dict(zip_longest(inputs_without_value,
                                              map(lambda x: tuple(map(parse_dimension, x.split(','))) if x else None,
                                                  shapes)))
        for inp in inputs:
            if '->' not in inp:
                inputs_list.append(inp)
                continue
            shape = placeholder_shapes[inp.split('->')[0]]
            inputs_list.append(inp.split('->')[0])

            if shape is None:
                continue
            for dim in shape:
                if isinstance(dim, tuple) or dim == -1:
                    raise Error("Cannot freeze input with dynamic shape: {}".format(shape))

    elif argv_input:
        raise Error('Please provide each input layers with an input layer shape. ' + refer_to_faq_msg(58))

    return inputs_list, placeholder_shapes, placeholder_data_types


def parse_tuple_pairs(argv_values: str):
    """
    Gets mean/scale values from the given string parameter
    Parameters
    ----------
    argv_values
        string with a specified input name and  list of mean values: either an empty string, or a tuple
        in a form [] or ().
        E.g. 'data(1,2,3)' means 1 for the RED channel, 2 for the GREEN channel, 3 for the BLUE channel for the data
        input layer, or tuple of values in a form [] or () if input is specified separately, e.g. (1,2,3),[4,5,6].

    Returns
    -------
        dictionary with input name and tuple of values or list of values if mean/scale value is specified with input,
        e.g.:
        "data(10,20,30),info(11,22,33)" -> { 'data': [10,20,30], 'info': [11,22,33] }
        "(10,20,30),(11,22,33)" -> [mo_array(10,20,30), mo_array(11,22,33)]
    """
    res = {}
    if not argv_values:
        return res

    matches = [m for m in re.finditer(r'[(\[]([0-9., -]+)[)\]]', argv_values, re.IGNORECASE)]

    error_msg = 'Mean/scale values should consist of name and values specified in round or square brackets ' \
                'separated by comma, e.g. data(1,2,3),info[2,3,4],egg[255] or data(1,2,3). Or just plain set of ' \
                'values without names: (1,2,3),(2,3,4) or [1,2,3],[2,3,4].' + refer_to_faq_msg(101)
    if not matches:
        raise Error(error_msg, argv_values)

    name_start_idx = 0
    name_was_present = False
    for idx, match in enumerate(matches):
        input_name = argv_values[name_start_idx:match.start(0)]
        name_start_idx = match.end(0) + 1
        tuple_value = np.fromstring(match.groups()[0], dtype=float, sep=',')

        if idx != 0 and (name_was_present ^ bool(input_name)):
            # if node name firstly was specified and then subsequently not or vice versa
            # e.g. (255),input[127] or input(255),[127]
            raise Error(error_msg, argv_values)

        name_was_present = True if input_name != "" else False
        if name_was_present:
            res[input_name] = tuple_value
        else:
            res[idx] = tuple_value

    if not name_was_present:
        # return a list instead of a dictionary
        res = sorted(res.values(), key=lambda v: v[0])
    return res


def get_tuple_values(argv_values: str or tuple, num_exp_values: int = 3, t=float or int):
    """
    Gets mean values from the given string parameter
    Args:
        argv_values: string with list of mean values: either an empty string, or a tuple in a form [] or ().
        E.g. '(1,2,3)' means 1 for the RED channel, 2 for the GREEN channel, 4 for the BLUE channel.
        t: either float or int
        num_exp_values: number of values in tuple

    Returns:
        tuple of values
    """

    digit_reg = r'(-?[0-9. ]+)' if t == float else r'(-?[0-9 ]+)'

    assert num_exp_values > 1, 'Can not parse tuple of size 1'
    content = r'{0}\s*,{1}\s*{0}'.format(digit_reg, (digit_reg + ',') * (num_exp_values - 2))
    tuple_reg = r'((\({0}\))|(\[{0}\]))'.format(content)

    if isinstance(argv_values, tuple) and not len(argv_values):
        return argv_values

    if not len(argv_values) or not re.match(tuple_reg, argv_values):
        raise Error('Values "{}" cannot be parsed. ' +
                    refer_to_faq_msg(59), argv_values)

    mean_values_matches = re.findall(r'[(\[]([0-9., -]+)[)\]]', argv_values)

    for mean in mean_values_matches:
        if len(mean.split(',')) != num_exp_values:
            raise Error('{} channels are expected for given values. ' +
                        refer_to_faq_msg(60), num_exp_values)

    return mean_values_matches


def get_mean_scale_dictionary(mean_values, scale_values, argv_input: str):
    """
    This function takes mean_values and scale_values, checks and processes them into convenient structure

    Parameters
    ----------
    mean_values dictionary, contains input name and mean values passed py user (e.g. {data: np.array[102.4, 122.1, 113.9]}),
    or list containing values (e.g. np.array[102.4, 122.1, 113.9])
    scale_values dictionary, contains input name and scale values passed py user (e.g. {data: np.array[102.4, 122.1, 113.9]})
    or list containing values (e.g. np.array[102.4, 122.1, 113.9])

    Returns
    -------
    The function returns a dictionary e.g.
    mean = { 'data': np.array, 'info': np.array }, scale = { 'data': np.array, 'info': np.array }, input = "data, info" ->
     { 'data': { 'mean': np.array, 'scale': np.array }, 'info': { 'mean': np.array, 'scale': np.array } }

    """
    res = {}
    # collect input names
    if argv_input:
        inputs = argv_input.split(',')
    else:
        inputs = []
        if type(mean_values) is dict:
            inputs = list(mean_values.keys())
        if type(scale_values) is dict:
            for name in scale_values.keys():
                if name not in inputs:
                    inputs.append(name)

    # create unified object containing both mean and scale for input
    if type(mean_values) is dict and type(scale_values) is dict:
        if not mean_values and not scale_values:
            return res

        for inp_scale in scale_values.keys():
            if inp_scale not in inputs:
                raise Error("Specified scale_values name '{}' do not match to any of inputs: {}. "
                            "Please set 'scale_values' that correspond to values from input.".format(inp_scale, inputs))

        for inp_mean in mean_values.keys():
            if inp_mean not in inputs:
                raise Error("Specified mean_values name '{}' do not match to any of inputs: {}. "
                            "Please set 'mean_values' that correspond to values from input.".format(inp_mean, inputs))

        for inp in inputs:
            inp, port = split_node_in_port(inp)
            if inp in mean_values or inp in scale_values:
                res.update(
                    {
                        inp: {
                            'mean':
                                mean_values[inp] if inp in mean_values else None,
                            'scale':
                                scale_values[inp] if inp in scale_values else None
                        }
                    }
                )
        return res

    # user specified input and mean/scale separately - we should return dictionary
    if inputs:
        if mean_values and scale_values:
            if len(inputs) != len(mean_values):
                raise Error('Numbers of inputs and mean values do not match. ' +
                            refer_to_faq_msg(61))
            if len(inputs) != len(scale_values):
                raise Error('Numbers of inputs and scale values do not match. ' +
                            refer_to_faq_msg(62))

            data = list(zip(mean_values, scale_values))

            for i in range(len(data)):
                res.update(
                    {
                        inputs[i]: {
                            'mean':
                                data[i][0],
                            'scale':
                                data[i][1],

                        }
                    }
                )
            return res
        # only mean value specified
        if mean_values:
            data = list(mean_values)
            for i in range(len(data)):
                res.update(
                    {
                        inputs[i]: {
                            'mean':
                                data[i],
                            'scale':
                                None

                        }
                    }
                )
            return res

        # only scale value specified
        if scale_values:
            data = list(scale_values)
            for i in range(len(data)):
                res.update(
                    {
                        inputs[i]: {
                            'mean':
                                None,
                            'scale':
                                data[i]

                        }
                    }
                )
            return res
    # mean and/or scale are specified without inputs
    return list(zip_longest(mean_values, scale_values))


def get_model_name(path_input_model: str) -> str:
    """
    Deduces model name by a given path to the input model
    Args:
        path_input_model: path to the input model

    Returns:
        name of the output IR
    """
    parsed_name, extension = os.path.splitext(os.path.basename(path_input_model))
    return 'model' if parsed_name.startswith('.') or len(parsed_name) == 0 else parsed_name


def get_absolute_path(path_to_file: str) -> str:
    """
    Deduces absolute path of the file by a given path to the file
    Args:
        path_to_file: path to the file

    Returns:
        absolute path of the file
    """
    file_path = os.path.expanduser(path_to_file)
    if not os.path.isabs(file_path):
        file_path = os.path.join(os.getcwd(), file_path)
    return file_path


def isfloat(value):
    try:
        float(value)
        return True
    except ValueError:
        return False


def isbool(value):
    try:
        strtobool(value)
        return True
    except ValueError:
        return False


def isdict(value):
    try:
        evaluated = ast.literal_eval(value)
        return isinstance(evaluated, dict)
    except ValueError:
        return False


def convert_string_to_real_type(value: str):
    if isdict(value):
        return ast.literal_eval(value)

    values = value.split(',')
    for i in range(len(values)):
        value = values[i]
        if value.isdigit():
            values[i] = int(value)
        elif isfloat(value):
            values[i] = float(value)
        elif isbool(value):
            values[i] = strtobool(value)

    return values[0] if len(values) == 1 else values


def parse_transform(transform: str) -> list:
    transforms = []

    if len(transform) == 0:
        return transforms

    all_transforms = re.findall(r"([a-zA-Z0-9]+)(\[([^\]]+)\])*(,|$)", transform)

    # Check that all characters were matched otherwise transform key value is invalid
    key_len = len(transform)
    for transform in all_transforms:
        # In regexp we have 4 groups where 1st group - transformation_name,
        #                                  2nd group - [args],
        #                                  3rd group - args, <-- nested group
        #                                  4th group - EOL
        # And to check that regexp matched all string we decrease total length by the length of matched groups (1,2,4)
        # In case if no arguments were given to transformation then 2nd and 3rd groups will be empty.
        if len(transform) != 4:
            raise Error("Unexpected transform key structure: {}".format(transform))
        key_len -= len(transform[0]) + len(transform[1]) + len(transform[3])

    if key_len != 0:
        raise Error("Unexpected transform key structure: {}".format(transform))

    for transform in all_transforms:
        name = transform[0]
        args = transform[2]

        args_dict = {}

        if len(args) != 0:
            for arg in args.split(';'):
                m = re.match(r"^([_a-zA-Z]+)=(.+)$", arg)
                if not m:
                    raise Error("Unrecognized attributes for transform key: {}".format(transform))

                args_dict[m.group(1)] = convert_string_to_real_type(m.group(2))

        transforms.append((name, args_dict))

    return transforms


def check_available_transforms(transforms: list):
    """
    This function check that transformations specified by user are available.
    :param transforms: list of user specified transformations
    :return: raises an Error if transformation is not available
    """
    from openvino.tools.mo.back.offline_transformations import get_available_transformations
    available_transforms = get_available_transformations()

    missing_transformations = []
    for name, _ in transforms:
        if name not in available_transforms.keys():
            missing_transformations.append(name)

    if len(missing_transformations) != 0:
        raise Error('Following transformations ({}) are not available. '
                    'List with available transformations ({})'.format(','.join(missing_transformations),
                                                                      ','.join(available_transforms.keys())))
    return True


def check_positive(value):
    try:
        int_value = int(value)
        if int_value <= 0:
            raise ValueError
    except ValueError:
        raise argparse.ArgumentTypeError("expected a positive integer value")

    return int_value


def depersonalize(value: str, key: str):
    dir_keys = [
        'output_dir', 'extensions', 'saved_model_dir', 'tensorboard_logdir', 'caffe_parser_path'
    ]
    if not isinstance(value, str):
        return value
    res = []
    for path in value.split(','):
        if os.path.isdir(path) and key in dir_keys:
            res.append('DIR')
        elif os.path.isfile(path):
            res.append(os.path.join('DIR', os.path.split(path)[1]))
        else:
            res.append(path)
    return ','.join(res)


def get_meta_info(argv: argparse.Namespace):
    meta_data = {'unset': []}
    for key, value in argv.__dict__.items():
        if value is not None:
            value = depersonalize(value, key)
            meta_data[key] = value
        else:
            meta_data['unset'].append(key)
    # The attribute 'k' is treated separately because it points to not existing file by default
    for key in ['k']:
        if key in meta_data:
            meta_data[key] = ','.join([os.path.join('DIR', os.path.split(i)[1]) for i in meta_data[key].split(',')])
    return meta_data


def get_available_front_ends(fem=None):
    # Use this function as workaround to avoid IR frontend usage by MO
    if fem is None:
        return []
    available_moc_front_ends = fem.get_available_front_ends()
    if 'ir' in available_moc_front_ends:
        available_moc_front_ends.remove('ir')

    return available_moc_front_ends
